Skip to content

feat(vault): route PAM gated ciphers through the SDK (partial-cipher pivot) - #22171

Open
Hinton wants to merge 1 commit into
mainfrom
pam/vault-gating-seams
Open

feat(vault): route PAM gated ciphers through the SDK (partial-cipher pivot)#22171
Hinton wants to merge 1 commit into
mainfrom
pam/vault-gating-seams

Conversation

@Hinton

@Hinton Hinton commented Jul 31, 2026

Copy link
Copy Markdown
Member

🎟️ Tracking

Strategy pivot for PAM cipher gating. Supersedes #22168, #22169, #22170 (now closed). Depends on bitwarden/sdk-internal#1359.

⚠️ Draft / blocked on the SDK: consumes types (Cipher.partialData, the partial view flag, and the RestrictedCipherData field allowlist) from sdk-internal#1359. The @bitwarden/sdk-internal version bump lands once that publishes; until then CI typecheck fails on three lines (cipher.ts toSdkCipher/fromSdkCipher, cipher.view.ts fromSdkCipherView). Reviewable now against a local WASM link. The branch is already squashed to a single commit and rebased onto latest main; the SDK version bump is the only remaining pre-merge step.

📔 Objective

Route PAM-gated ("partial") ciphers through the SDK and surface them only in the web vault, where they render read-only with a "Controlled access" badge. A partial cipher is a row whose sensitive fields the server suppressed (the caller lacks a privileged-access lease); it ships a reduced partialData envelope (encrypted name + login URIs only) in place of the full payload, and the SDK decrypts it into a view marked partial.

Everywhere outside the web vault list, partials are excluded — they must never reach autofill, export, reports, Fido2, key rotation, or the CLI, and must never be modifiable (editing/re-encrypting a gated row would clobber the server-suppressed fields with blanks).

The security-critical field allowlist (a gated view exposes only name + login URIs, never password/TOTP) lives in the SDK (RestrictedCipherData in #1359), not in this repo.

What changed

Model plumbing. CipherResponse.partialData (optional) flows verbatim → CipherData → domain Cipher.partialData, round-tripping losslessly through toSdkCipher/fromSdkCipher. The SDK decrypts gated rows like any other cipher (no client-side partition, no SdkRecordMapper.shouldInclude filter, no deprecated Cipher.decrypt) and stamps partial: boolean on the decrypted CipherView/CipherListView.

Web-vault-only, excluded-by-default streams. CipherService splits its decrypted streams so partials are excluded by default and only the web list opts in:

  • getAllDecrypted() excludes partials at the root (a private getAllDecryptedIncludingPartials() retains them solely as the shared decrypt source), covering every imperative consumer — export, reports, Fido2, ssh-agent, autofill card/identity suggestions, key rotation, CLI.
  • cipherViews$ / cipherListViews$ exclude partials, derived from private partials-inclusive sources so decryption stays single-pass.
  • The web vault list consumes the new opt-in cipherListViewsWithPartials$.
  • CipherViewLikeUtils.isPartial centralizes the flag read.

Read-only rendering + badge seams. A gated cipher opens read-only in the vault-item dialog (Edit hidden, form-mode redirected to view) and cipher-view, where a host can render an optional banner through the CIPHER_VIEW_BANNER seam (handed the CipherView directly). A host-provided "Controlled access" badge column (VAULT_ROW_LEASE_BADGE seam) appears only when the Pam feature flag is on, a PAM-enabled org (Organization.usePam) is in view, and a host provides the badge — otherwise the table is unchanged.

Non-modifiable in web. Gated rows are non-selectable (checkbox disabled + excluded from editableItems/select-all) and expose no modify menu actions; both bulk-action paths (classic emitters + VaultBatchBarService) filter them defensively; encrypted export excludes them via the partialData marker.

Filter tree. The web filter (type filter + folder tree, via a web-only VaultFilterService subclass) includes partials so a type/folder present only as gated rows still surfaces; other clients keep the excluded stream. Collections are unaffected.

Verification

test:types, lint, and prettier are clean on the affected projects (save the three SDK-blocked lines above). Touched + model specs pass, including new coverage for the stream split/exclusion, isPartial, the read-only rendering, the badge column gate, non-selectability, bulk-action + export exclusion, and key-rotation exclusion. E2E stays a no-op until a real PAM provider binds the badge seam and the server emits partialData.

🚨 Breaking Changes

None — inert until a PAM provider binds the badge seam and the server emits partialData. With PAM off, no cipher is partial, so every added filter is a no-op and behavior is unchanged.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.68966% with 17 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (pam/cipher-partial-decrypt@ad7806d). Learn more about missing BASE report.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...c/vault-item-dialog/vault-item-dialog.component.ts 55.26% 15 Missing and 2 partials ⚠️
Additional details and impacted files
@@                      Coverage Diff                      @@
##             pam/cipher-partial-decrypt   #22171   +/-   ##
=============================================================
  Coverage                              ?   52.74%           
=============================================================
  Files                                 ?     4207           
  Lines                                 ?   133252           
  Branches                              ?    20977           
=============================================================
  Hits                                  ?    70284           
  Misses                                ?    57829           
  Partials                              ?     5139           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Hinton Hinton changed the title feat(vault): add the seams for rendering and revealing a gated cipher feat(vault): route PAM gated ciphers through the SDK (partial-cipher pivot) Aug 6, 2026
@Hinton
Hinton changed the base branch from pam/cipher-partial-decrypt to main August 6, 2026 17:00
@Hinton
Hinton force-pushed the pam/vault-gating-seams branch from 6771120 to a0c1558 Compare August 6, 2026 18:32
Comment thread apps/web/src/app/vault/components/vault-items/vault-items.component.ts Outdated
Comment thread apps/web/src/app/vault/individual-vault/cipher-open-gate.ts Outdated
Comment thread libs/common/src/platform/services/sdk/client-managed-state.ts Outdated
Comment thread libs/common/src/vault/models/data/cipher.data.ts Outdated
Comment thread libs/common/src/vault/models/response/cipher.response.ts Outdated
Comment thread libs/vault/src/tokens/gated-cipher-reloader.token.ts Outdated
Comment thread libs/vault/src/cipher-view/cipher-view.component.html Outdated
Comment thread libs/common/src/vault/services/cipher.service.ts Outdated
Comment thread libs/common/src/vault/services/cipher.service.ts Outdated
Comment thread libs/common/src/vault/services/cipher.service.ts
Comment thread libs/common/src/vault/models/domain/cipher.ts
@Hinton
Hinton force-pushed the pam/vault-gating-seams branch from fc9198d to 2dfba20 Compare August 14, 2026 09:26
@Hinton Hinton added the t:feature Change Type - Feature Development label Aug 14, 2026
@Hinton
Hinton force-pushed the pam/vault-gating-seams branch from 2dfba20 to 76b9e53 Compare August 14, 2026 09:29
@Hinton
Hinton marked this pull request as ready for review August 17, 2026 07:37
@Hinton
Hinton requested review from a team as code owners August 17, 2026 07:37
@Hinton
Hinton requested a review from nick-livefront August 17, 2026 07:37
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the partial-cipher pivot: the partialData plumbing (CipherResponseCipherDataCipher → SDK round-trip), the CipherService stream split (partials-excluded by default, opt-in cipherListViewsWithPartials$ for the web list and web folder/type filters), and the web read-only rendering, badge-column seam, non-selectability, bulk-action, and export exclusions. I traced every consumer of getAllDecrypted / cipherViews$ / cipherListViews$ and confirmed export, reports, Fido2, ssh-agent, autofill, key rotation, and the CLI now see partials excluded, that viewCipherById and DefaultCipherFormConfigService read encrypted state (so gated rows still open), and that all VaultBatchBarService actions route through the filtered selectedCiphers(). Copy actions self-hide on gated rows via CopyCipherFieldDirective, and the new "Controlled access" column's header/cipher-row/collection-row cells stay in sync.

Code Review Details
  • ♻️ : CipherView.leaseGated has no producer or consumer in this repo (only set, never asserted, in one spec); doc also claims it is never serialized while toJSON() returns this
    • libs/common/src/vault/models/view/cipher.view.ts:80-86
  • 🎨 : Read paths have a single partial-exclusion choke point; the write path relies only on UI filters — a guard in encrypt would make silent clobbering of suppressed fields impossible
    • libs/common/src/vault/services/cipher.service.ts:400
  • ❓ : Delete/Archive still render for a partial cipher in the item dialog, while the row menu and both bulk paths deliberately block them — intentional?
    • libs/vault/src/vault-item-dialog/vault-item-dialog.component.ts:275

Note: as the description states, this stays blocked on the @bitwarden/sdk-internal bump for Cipher.partialData and the partial view flag; the three affected lines can't type-check until then.

return this.showCipherView && !this.isTrashFilter && !this.showRestore;
// Hide Edit for a partial-data cipher — the host banner owns the access flow until
// the full cipher is revealed.
return this.showCipherView && !this.isTrashFilter && !this.showRestore && !this.isPartialData;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QUESTION: Should Delete/Archive also be hidden for a partial cipher in this dialog?

Details

showEdit now excludes partials, but the footer's other mutating actions don't:

  • showActionButtonsthis.cipher !== null && mode !== "clone", so the trash button renders and is enabled whenever canDelete (i.e. cipher.permissions?.delete) is true.
  • showArchiveOptions!isAdminConsoleAction && mode === "view", so archive/unarchive can render too.

The row menu in vault-cipher-row.component.html deliberately hides delete, restore, archive, unarchive, and favorite for isPartial, and both bulk paths filter partials out — so clicking a gated row's name opens the one surface that still offers those actions.

If gated rows should expose no modify action at all, gating showActionButtons (or canDelete / showArchiveOptions) on isPartialData would make the dialog consistent with the row and bulk paths.

Comment on lines +80 to +86
/**
* Client-only, transient companion to {@link partial}: set on a full cipher served under an
* active PAM lease (full data, so `partial` is false). Never sent by the server, persisted,
* or serialized — its producer (the leased-cipher fetcher) stamps it directly on the view.
* Lets gating surfaces keep rendering lease state once a lease lands.
*/
leaseGated?: boolean;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ DEBT: leaseGated has no producer or consumer in this repo — consider landing it with the lease fetcher.

Details

The only reference outside this declaration is cipher-view.component.spec.ts:220, which sets it but never asserts on it. Nothing reads it, so the field is inert until the "leased-cipher fetcher" it documents exists. Adding it to a core model now means every consumer of CipherView sees a field they can't act on.

Also, the doc says it is never "persisted, or serialized": toJSON() returns this, so a stamped leaseGated is written to the DECRYPTED_CIPHERS state blob — it just doesn't survive fromJSON. Worth rewording if the field stays.

Comment thread libs/common/src/vault/services/cipher.service.ts
@Hinton
Hinton force-pushed the pam/vault-gating-seams branch 2 times, most recently from 5060ea7 to b49db25 Compare August 18, 2026 08:28
…pivot)

Route PAM-gated ("partial") ciphers through the SDK and surface them only in
the web vault, where they render read-only with a "Controlled access" badge. A
partial cipher ships a reduced `partialData` envelope in place of its full
payload; the SDK decrypts it into a view marked `partial`. Everywhere outside
the web vault list, partials are excluded — they never reach autofill, export,
reports, Fido2, key rotation, or the CLI, and are never modifiable.

Model plumbing: `CipherResponse.partialData` flows verbatim through `CipherData`
to domain `Cipher.partialData`, round-tripping losslessly through
`toSdkCipher`/`fromSdkCipher`.

Excluded-by-default streams: `getAllDecrypted()` and `cipherViews$` /
`cipherListViews$` exclude partials, derived from private partials-inclusive
sources so decryption stays single-pass. The web list consumes the opt-in
`cipherListViewsWithPartials$`. `CipherViewLikeUtils.isPartial` centralizes the
flag read.

Read-only rendering + badge seam: a gated cipher opens read-only in the
vault-item dialog and cipher-view; a host-provided "Controlled access" badge
column (`VAULT_ROW_LEASE_BADGE`) appears only when the `Pam` flag is on, a
`usePam` org is in view, and a host provides the badge.

Non-modifiable in web: gated rows are non-selectable and expose no modify menu
actions; both bulk-action paths filter them defensively; encrypted export
excludes them. The web filter tree includes partials so a type/folder present
only as gated rows still surfaces.

Inert until a PAM provider binds the badge seam and the server emits
`partialData`; with PAM off, every added filter is a no-op.
@Hinton
Hinton force-pushed the pam/vault-gating-seams branch from b49db25 to 3dd58dd Compare August 18, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant